今天是超級自信之作哈哈哈
🏮 今天完整的程式碼可以拉到最底下 Put it together 區塊或是在 GitHub 找到。
在實作任何程式邏輯之前,我們先來定義一個結構以代表使用者與 LLM 之間的對話。
這在 Rust 中可以透過自訂 Struct 型別來完成,它又稱為結構體 (structure),可以用來把各種相關的數值組合起來成為一個自訂的型別,以物件導向的概念來說,Struct 就像是物件的資料屬性 (attribute)。
將程式碼模組化能把相關的功能組織起來,並依照功能分門別類,如此一來,我們就能清楚地知道實作特定功能的程式碼在哪。
而 Rust 提供一系列管理程式碼組織的功能,其中包含了哪些實作細節能對外公開、哪些細節是私有的,以及程式中每個作用域的名稱為何,這些功能統一稱作模組系統 (module system),其中包含:
use: 讓你控制組織、作用域與路徑的隱私權這裡簡略說明模組、路徑、use 與 pub 關鍵字在編譯器如何運作:
從 crate 源頭開始:編譯 crate 時,編譯器會先尋找 crate 的源頭檔案來編譯程式碼。
若為函式庫 crate 一般是指 src/lib.rs,執行檔 crate 則是指 src/main.rs。
crate 是 Rust 編譯器在當下視為程式碼的最小單位,例如每次初始化專案會給的
main.rs檔案。
crate 有兩種形式:執行檔 (Binary) crate 或函式庫 (Library) crate。
前者是能編譯成執行檔並執行的程式,通常需要main函式來定義在執行時該做什麼事。
後者則不會有main函式也不會被編譯成執行檔,就跟我們熟悉的函式庫功能相同。
宣告模組:在 crate 源頭檔案中,可以使用 mod 關鍵字宣告新的模組。
例如我們宣告了一個「LycoReco」模組 mod LycoReco;。
編譯器會在下面這幾個地方尋找模組的程式碼:
mod LycoReco {...} 區塊中src/LycoReco.rs 檔案中src/LycoReco/mod.rs 檔案中
這是比較舊的路徑風格,使用這種風格最大的缺點就是專案中有大量名為 mod.rs 的檔案,同時開啟時很容易混淆。
宣告子模組:除了 crate 源頭之外,其他檔案也可以宣告子模組。
例如我們可以在 src/LycoReco.rs 中宣告 mod closet;。
編譯器會與當前模組同名的目錄底下這幾處尋找子模組的程式碼:
mod closet {...} 區塊中src/LycoReco/closet.rs 檔案中src/LycoReco/closet/mod.rs 檔案中模組的路徑:一旦模組成為 crate 的一部分,只要隱私權規則允許,其程式碼可以在 crate 內任意地方被使用。
例如「LycoReco」模組下「closet」模組中的 Walnut 型別可以用 crate::LycoReco::closet::Walnut 來找到。
私有 vs 公開:模組內的程式碼從上層模組來看預設是私有的。要公開的話,必須將它宣告為 pub mod,而公開模組內的項目也可以用 pub 來公開。
use 關鍵字:在一個作用域內,use 關鍵字可以建立項目的捷徑,來縮短冗長的路徑名稱。
而上面的範例專案就可以包含以下這些檔案與資料夾:
.
├── Cargo.lock
├── Cargo.toml
└── src
    ├── LycoReco
    │   └── closet.rs
    ├── closet.rs
    └── main.rs

而根據上面的說明,我們可以先在 src 資料夾中建立一個 model 資料夾與 model.rs,然後在 model 資料夾中加上 conversation.rs。
然後在 model.rs  的上方加上 pub mod conversation; 與 lib.rs 的 pub mod app; 下面加上 pub mod model; 來將這些模組加入專案的模組樹中。
所以此時 src 資料夾的結構如下:
.
└── src
    ├── model
    │   └── conversation.rs
    ├── model.rs
    ├── lib.rs
    ├── app.rs
    └── main.rs
在建立代表對話的結構體之前,我們首先需要另一個結構體來代表訊息本身,這裡將其命名為 Message:
pub struct Message {
    pub user: bool,
    pub text: String,
}
其中有兩個欄位 (fields),user 以布林值區分是否為使用者輸入的訊息,而 text 則為訊息本身。
有了代表訊息的結構體後,就可以建立代表對話的結構體了,我們將其命名為 Conversation,它只有一個欄位,就是以 Message 為元素向量:
pub struct Conversation {
    pub messages: Vec<Message>,
}
為了能更輕鬆地產生新的 Conversation 實例,而非每次都要建立一個空向量:我們可以實作一個 關聯函式 作為建構子:
impl Conversation {
    pub fn new() -> Conversation {
        Conversation {
            messages: Vec::new(),
        }
    }
}
如此一來,之後要建立新對話時,只需要使用 Conversation::new() 即可。
因為這些結構體會在客戶端與伺服器端傳遞,所以必須有將其序列化與反序列化的方法。
而在 Rust 中可以透過 serde crate 來達成。
這時候可以使用 cargo add serde -F derive  或直接到 Cargo.toml 檔 dependencies 區塊加上
`serde = { version = "1.0.188", features = ["derive"] }`
這裡特別註明要使用了 derive 巨集的功能,所以只要在結構體上加上 #[derive(Serialize, Deserialize)] 就能讓該型別自動實作序列化與反序列化。
所以最後 conversation.rs 的程式碼整理如下:
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Conversation {
    pub messages: Vec<Message>,
}
impl Conversation {
    pub fn new() -> Conversation {
        Conversation {
            messages: Vec::new(),
        }
    }
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Message {
    // distinguish the message from the LLM and the user
    pub user: bool,
    // the message
    pub text: String,
}
有了可以代表對話的資料結構之後,明天就可以開始實作一些程式邏輯了,明天見啦~